package com.atguigu.gulimall.seckill.service.impl;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.to.SeckillOrderTo;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberResVo;
import com.atguigu.gulimall.seckill.feign.CouponFeignService;
import com.atguigu.gulimall.seckill.feign.ProductFeignService;
import com.atguigu.gulimall.seckill.interceptor.LoginUserInterceptor;
import com.atguigu.gulimall.seckill.service.SeckillService;
import com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo;
import com.atguigu.gulimall.seckill.vo.SeckillSessionWithSku;
import com.atguigu.gulimall.seckill.vo.SeckillSkuVo;
import com.atguigu.gulimall.seckill.vo.SkuInfoVo;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Author 小坏
 * @Date 2021/2/25 20:19
 * @Version 1.0
 * @program: 父工程 gulimall 万物起源之地
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redissonClient; //分布式信号量

    @Autowired
    private RabbitTemplate rabbitTemplate;


    //活动的缓存前缀
    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";

    private final String SKUKILL_CACHE_PREFIX = "seckill:skus";

    //SKU库存的信号量
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码、拿id扣肯定不行

    /**
     * 扫描最近三天需要参与秒杀的活动
     */
    @Override
    public void uploadSecKillSkuLaTes3Days3() {
        //扫描最近三天需要参与秒杀的活动
        R session = couponFeignService.getLates3DaySession ();
        if (session.getCode () == 0) {
            //获取到这个活动下所有上架的商品
//            List<SeckillSessionWithSku> sessionDta = session.getDta ("",new TypeReference<List<SeckillSessionWithSku>> () {
//            });

            List<SeckillSessionWithSku> sessionDta = session.getData ("data", new TypeReference<List<SeckillSessionWithSku>> () {
            });
            System.out.println ("扫描最近三天需要参与秒杀的活动"+sessionDta);

            //缓存到redis
            //1、缓存活动信息
            saveSessionInfos (sessionDta);
            //2、缓存活动的关联商品信息
            saveSessionSkuInfo (sessionDta);
        }

    }

    public List<SeckillSkuRedisTo> blockHandler(){
        log.error ("blockHandler","被限流了");
        return null;
    }


    /**
     * blockHandler 函数会在原方法被限流/降级/系统保护的时候调用，
     * 而fallback函数会针对所有类型的异常
     *
     * 如果想受保护 方法上面试接配置  @SentinelResource
     * 如果是一个请求、什么都不用馆、所有的请求都会被自动转发到
     * @return
     */
    //返回当前时间可以参与的秒杀商品信息
    @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {

        //确定当前时间属于那个秒杀场次
        //1970
        long time = new Date ().getTime ();

        try (Entry entry = SphU.entry ("seckillSkus")) {
            //        //拿到这个key的所有数据
            Set<String> keys = redisTemplate.keys (SESSIONS_CACHE_PREFIX + "*");
            System.out.println (keys.size ());
            for (String key : keys) {
                String replace = key.replace (SESSIONS_CACHE_PREFIX, "");
                String[] s = replace.split ("_"); //截取_
                long start = Long.parseLong (s[0]); //开始时间
                long end = Long.parseLong (s[1]); //结束时间
                //当前时间判断
                System.out.println (start + ":" + time + end);
                if (time >= start && time <= end) {
                    //获取这个秒杀场次需要的所有商品信息
                    List<String> range = redisTemplate.opsForList ().range (key, -100, 100);
                    //这个key里面存了很多商品的值、唉个获取
                    BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps (SKUKILL_CACHE_PREFIX);

                    List<String> list = hashOps.multiGet (range);//比较快的方法multiGet、可以直接指定获取多个key的数据

                    System.out.println ("redis的key*" + list);
                    if (list != null && list.size () >= 0) {

                        List<SeckillSkuRedisTo> collect = list.stream ().map (items -> {
                            String item = (String) items;
                            SeckillSkuRedisTo redisTo = JSON.parseObject (item, SeckillSkuRedisTo.class);
//                       redis.setRandomCode (null); 当前秒杀开始就需要随机码
                            return redisTo;
                        }).collect (Collectors.toList ());
                        return collect;
                    }
                    break;
                }
            }
        } catch (BlockException e) {
            log.info ("资源被限制了{}" + e.getMessage ());
        }
        return null;

    }

    /**
     * 获取某一个sku商品的秒杀预告信息
     *
     * @return
     */
    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        //1、找到所有需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps (SKUKILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys ();
        if (keys != null && keys.size () > 0) {
            String regx = "\\d-" + skuId;
            for (String key : keys) {
                //6_4
                if (Pattern.matches (regx, key)) {
                    String json = hashOps.get (key);
                    SeckillSkuRedisTo skuRedisTo = JSON.parseObject (json, SeckillSkuRedisTo.class);

                    //随机码
                    long current = new Date ().getTime ();
                    if (current >= skuRedisTo.getStartTime () && current <= skuRedisTo.getEndTime ()) {

                    } else {
                        skuRedisTo.setRandomCode (null);
                    }
                    return skuRedisTo;
                }
            }
        }

        return null;
    }

    /**
     * 使用第二套秒杀流程
     * 结合了消息队列、此时这块不是要做最终一致性（虽然也要保证最终一致性）
     * 但是更多要做的是流量萧峰、请求一进来不用立即调用订单服务、放到MQ里面订单服务慢慢去消费、就不至于把订单服务打垮
     * 要打垮也只能打垮一个订单服务（这是一个优点）
     *
     * @param killId
     * @param key
     * @param num
     * @return TODO 上架秒杀商品的时候，每个数据都有过期时间
     * TODO 秒杀后续流程，简化了收获地址信息
     */
    @Override
    public String seckill(String killId, String key, Integer num) {
        long l = System.currentTimeMillis ();

        //拦截器能得到当前用户的所有信息
        MemberResVo loinUser = LoginUserInterceptor.loinUser.get ();

        //获取当前秒杀商品信息------->绑定以一个hashKey
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps (SKUKILL_CACHE_PREFIX);
        String json = hashOps.get (killId);
        //获取不到key
        if (StringUtils.isEmpty (json)) {
            return null;
        } else {
            //获取到操作
            SeckillSkuRedisTo redis = JSON.parseObject (json, SeckillSkuRedisTo.class);
            //TODO 1、校验合法性
            Long startTime = redis.getStartTime ();//开始时间
            Long endTime = redis.getEndTime ();//结束时间
            Long time = new Date ().getTime (); //当前时间

            long ttl = endTime - time; //时间差
            //判断当前时间在我们这个时间区间内、就是秒杀可以用的时间
            if (time > startTime && time < endTime) {
                //TODO 2、校验随机码、和商品id
                String randomCode = redis.getRandomCode ();//随机码
                String skuId = redis.getPromotionSessionId () + "-" + redis.getSkuId (); //商品id
                if (randomCode.equals (key) && killId.equals (skuId)) {
                    //TODO 3、验证购物数量是否合理
                    System.out.println(redis.getSeckillLimit ());
                    if (num <= redis.getSeckillLimit ()) {
                        //TODO 4、验证这个人是否已经买过了。幂等性；如果只要秒杀成功，就去站位 ,使用userId_sessionId_skuId 作为一个key 买过的
                        //SETNX------>setIfAbsent(不存在的时候才占位、最终返回占位成功、占位失败)
                        String redisKey = loinUser.getId () + "-" + skuId;
                        //key :redisKey, value: num.toString ()可以看到买了几件、占位不是永久占位的
                        //时间：这个当前场次以结束、这个东西自动清空 MILLISECONDS毫秒单位
                        Boolean aBoolean = redisTemplate.opsForValue ().setIfAbsent (redisKey, num.toString (), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            //占位成功说明从未买过------>获取分布式信号量
                            RSemaphore semaphore = redissonClient.getSemaphore (SKU_STOCK_SEMAPHORE + randomCode);

                            //从信号量拿出来买的num、
//                                boolean b = semaphore.tryAcquire (num, 100, TimeUnit.MILLISECONDS);
                            boolean b = semaphore.tryAcquire (num); //不要等待时间、没用任何异常
                            if (b) {
                                //拿到信号量、秒杀成功
                                //快速下单。发送MQ消息
                                SeckillOrderTo orderTo = new SeckillOrderTo ();

                                //封装秒杀的消息对象
                                String timeId = IdWorker.getTimeId ();
                                orderTo.setOrderSn (timeId);
                                orderTo.setMemberId (loinUser.getId ());
                                orderTo.setNum (num);
                                orderTo.setPromotionSessionId (redis.getPromotionSessionId ());
                                orderTo.setSkuId (redis.getSkuId ());
                                orderTo.setSeckillPrice (redis.getSeckillPrice ());
                                //消息队列萧峰处理
                                rabbitTemplate.convertAndSend ("order-event-exchange", "order.seckill.order", orderTo);
                                long ls = System.currentTimeMillis ();
                                log.info ("秒杀耗时。。。", (l - ls)); //12毫秒、（一个线程可以处理）一秒一百个请求
                                return timeId;
                            }
                            return null;

                        } else {
                            //说明已经买过了
                            return null;
                        }
                    }
                } else {
                    return null;
                }
            } else {
                return null;
            }
        }

        return null;
    }

    //活动信息---------》场次
    private void saveSessionInfos(List<SeckillSessionWithSku> sessionDta) {
        if (sessionDta != null)
            sessionDta.stream ().forEach (session -> {
                Long startTime = session.getStartTime ().getTime ();
                Long endTime = session.getEndTime ().getTime ();
                //定义key :前缀+开始时间_结束时间
                String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
                //查询key是否存在
                Boolean haskey = redisTemplate.hasKey (key);

                //没有这个key才来做这件事情
                if (!haskey) {
                    //getRelationEntities 关联的所有商品id
                    List<SeckillSkuVo> relationSkus = session.getRelationEntities ();
                    System.out.println ("关联的所有商品id"+relationSkus);
                    List<String> collect = session.getRelationEntities ().stream ()
                            .map (item -> item.getPromotionSessionId () + "-" + item.getSkuId ().toString ()).collect (Collectors.toList ());
                    //缓存活动信息
                    redisTemplate.opsForList ().leftPushAll (key, collect);//从左边往里放一整个集合
                }

            });

    }

    /**
     * 缓存秒杀活动所关联的商品信息
     *
     * @param //库存
     */

    private void saveSessionSkuInfo(List<SeckillSessionWithSku> sessionDta) {
        //准备hash操作
        sessionDta.stream ().forEach (session -> {
            //绑定一个hash操作
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps (SKUKILL_CACHE_PREFIX);
            session.getRelationEntities ().stream ().forEach (seckillSkuVo -> {

                String token = UUID.randomUUID ().toString ().replace ("-", "");
                //不存在、再来进行保存
                String redisKey = seckillSkuVo.getPromotionSessionId ().toString () + "-" + seckillSkuVo.getSkuId ().toString ();
                if (!ops.hasKey (redisKey)) {
                    //缓存商品
                    SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo ();

                    //1、sku基本数据
                    R skuInfo = productFeignService.getSkuInfo (seckillSkuVo.getSkuId ());
                    if (skuInfo.getCode () == 0) {
                        SkuInfoVo info = skuInfo.getData ("skuInfo", new TypeReference<SkuInfoVo> () {
                        });
                        redisTo.setSkuInfoVo (info);
                    }

                    //2、sku的秒杀信息
                    BeanUtils.copyProperties (seckillSkuVo, redisTo);
                    //3、设置上当前商品的秒杀时间信息
                    redisTo.setStartTime (session.getStartTime ().getTime ());
                    redisTo.setEndTime (session.getEndTime ().getTime ());

                    /**
                     * 4、设置随机码？seckill?skuId&key=dadlajldj、
                     *      想要来秒杀你不知道随机码、商品id知道、发请求也没用、随机码只要秒杀开启的时候才暴漏出来
                     *
                     *   设置信号量： 每个请求进来要减redis的计数、这个信号量的值就是商品库存的信息、比如库存100、那计算器就是100
                     *   问题：如果带拿商品id过来匹配的一个数量（计数器）、直接去redis中查到了、那就不合适、所以准备一个随机码、
                     *   只要到了秒杀时间了、然后你知道这个随机码、带上这个随机码、我是按照随机码减信号量的
                     *
                     * 如果不带随机码、每次按照skuid来减信号量、会出问题、秒杀还没开始、恶意请求就把信号量减去了、这个随机码又是一种保护机制、
                     * 买商品这个人知道我的随机码才可以买这个商品、减掉库存
                     *
                     * 每一个商品都要设置他的分布式信号量
                     *
                     */
                    redisTo.setRandomCode (token);//设置随机码
                    String jsonString = JSON.toJSONString (redisTo);

                    //当前商品这个jsonString的信息
                    ops.put (seckillSkuVo.getPromotionSessionId ().toString () + "-" + seckillSkuVo.getSkuId ().toString (), jsonString);

                    /**
                     * 5、使用库存作为分布式的信号量、秒杀请求一进来他的信号量都获取不到、你都不用执行我们数据库的方法了
                     *        秒杀要做的最大的工作就是限流
                     */
                    RSemaphore semaphore = redissonClient.getSemaphore (SKU_STOCK_SEMAPHORE + token);//信号量前缀+随机码
                    //商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits (seckillSkuVo.getSeckillCount ());

                }

            });
        });
    }
}
